#include <tsinide/CodeEditor.hpp>
#include <tsinide/LineNumberArea.hpp>

#include <QtGui>

Highlighter::Highlighter(QTextDocument *parent)
        : QSyntaxHighlighter(parent)
{
        HighlightingRule rule;

        headerFormat.setForeground(Qt::darkMagenta);
        keywordFormat.setForeground(Qt::darkBlue);
        keywordFormat.setFontWeight(QFont::Bold);
        numberFormat.setForeground(Qt::blue);
        typeFormat.setForeground(Qt::blue);
        typeFormat.setFontItalic(true);
        commentFormat.setForeground(Qt::darkGreen);

        /* Imported headers */
        rule.pattern = QRegExp("Import [A-Za-z0-9 ]+");
        rule.format = headerFormat;
        highlightingRules.append(rule);

        /* Keywords */
        QStringList keywordPatterns;
        keywordPatterns << "\\bAssume\\b" << "\\bDefine\\b" << "\\bBind\\b"
                        << "\\bInductive\\b"  << "\\bRecord\\b" << "\\bImport\\b";
        foreach (const QString &pattern, keywordPatterns) {
                rule.pattern = QRegExp(pattern);
                rule.format = keywordFormat;
                highlightingRules.append(rule);
        }

        /* Numbers */
        rule.pattern = QRegExp("\\b[0-9]+([.][0-9]+)?\\b");
        rule.format = numberFormat;
        highlightingRules.append(rule);

        /* Types */
        rule.pattern = QRegExp(": *[A-Za-z][A-Za-z0-9_]+");
        rule.format = typeFormat;
        highlightingRules.append(rule);
        rule.pattern = QRegExp("\\:");
        rule.format = normalFormat;
        highlightingRules.append(rule);

        /* Comments */
        commentStartExpression = QRegExp("\\(\\*");
        commentEndExpression = QRegExp("\\*\\)");
}

void Highlighter::highlightBlock(const QString &text)
{
        /* Applying predefined rules */
        foreach (const HighlightingRule &rule, highlightingRules) {
                QRegExp exp(rule.pattern);
                int index = exp.indexIn(text);
                while (index >= 0) {
                        int length = exp.matchedLength();
                        setFormat(index, length, rule.format);
                        index = exp.indexIn(text, index + length);
                }
        }

        /* Recognize multiline comments */
        setCurrentBlockState(NOT_IN_COMMENT);
        int startIndex = 0;
        if (previousBlockState() != IN_COMMENT)
                startIndex = commentStartExpression.indexIn(text);
        while (startIndex >= 0) {
                int endIndex = commentEndExpression.indexIn(text, startIndex);
                int commentLength;
                if (endIndex == -1) {
                        setCurrentBlockState(IN_COMMENT);
                        commentLength = text.length() - startIndex;
                } else {
                        commentLength = endIndex - startIndex + commentEndExpression.matchedLength();
                }
                setFormat(startIndex, commentLength, commentFormat);
                startIndex = commentStartExpression.indexIn(text, startIndex + commentLength);
        }
}

CodeEditor::CodeEditor(QWidget *parent)
        : QPlainTextEdit(parent)
{
        lineNumberArea = new LineNumberArea(this);
        highlighter = new Highlighter(document());

        connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
        connect(this, SIGNAL(updateRequest(QRect, int)), this, SLOT(updateLineNumberArea(QRect, int)));
        connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));

        updateLineNumberAreaWidth(0);
        highlightCurrentLine();
}

CodeEditor::~CodeEditor()
{
        delete highlighter;
        delete lineNumberArea;
}

int CodeEditor::lineNumberAreaWidth()
{
        static const int extraSpace = 8;
        int digits = 1;
        int max = qMax(1, document()->blockCount());
        while (max >= 10) {
                max /= 10;
                ++digits;
        }

        int space = extraSpace + fontMetrics().width(QLatin1Char('9')) * digits;

        return space;
}

void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{
        QPainter painter(lineNumberArea);
        painter.fillRect(event->rect(), Qt::lightGray);

        QTextBlock block = firstVisibleBlock();
        int blockNumber = block.blockNumber();
        int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
        int bottom = top + (int) blockBoundingRect(block).height();

        while (block.isValid() && top <= event->rect().bottom()) {
            if (block.isVisible() && bottom >= event->rect().top()) {
                QString number = QString::number(blockNumber + 1);
                painter.setPen(Qt::black);
                painter.drawText(0, top, lineNumberArea->width(), fontMetrics().height(),
                                 Qt::AlignRight, number);
            }

            block = block.next();
            top = bottom;
            bottom = top + (int) blockBoundingRect(block).height();
            ++blockNumber;
        }
}

void CodeEditor::resizeEvent(QResizeEvent *e)
{
        QPlainTextEdit::resizeEvent(e);

        QRect cr = contentsRect();
        lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}

void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
{
        setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}

void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
{
        if (dy)
                lineNumberArea->scroll(0, dy);
        else
                lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());

        if (rect.contains(viewport()->rect()))
                updateLineNumberAreaWidth(0);
}

void CodeEditor::highlightCurrentLine()
{
        QList<QTextEdit::ExtraSelection> extraSelections;

        if (!isReadOnly()) {
                QTextEdit::ExtraSelection selection;

                QColor lineColor = QColor(Qt::yellow).lighter(160);

                selection.format.setBackground(lineColor);
                selection.format.setProperty(QTextFormat::FullWidthSelection, true);
                selection.cursor = textCursor();
                selection.cursor.clearSelection();
                extraSelections.append(selection);
        }

        setExtraSelections(extraSelections);
}
